As you are already aware, .NET types may support various members. Enumerations have some set of name/value pairs. Structures and classes may have constructors, fields, methods, properties, static members, and so on. Over the course of this book’s first 16 chapters, you have already seen partial CIL definitions for the items previously mentioned, but nevertheless, here is a quick recap of how various members map to CIL primitives.
Enumerations, structures, and classes can all support field data. In each case, the .field directive will be used. For example, let’s breathe some life into the skeleton MyEnum enumeration and define three name/value pairs (note the values are specified within parentheses):
.class public sealed enum MyEnum { .field public static literal valuetype MyNamespace.MyEnum A = int32(0) .field public static literal valuetype MyNamespace.MyEnum B = int32(1) .field public static literal valuetype MyNamespace.MyEnum C = int32(2) }
Fields that reside within the scope of a .NET System.Enum-derived type are qualified using the static and literal attributes. As you would guess, these attributes set up the field data to be a fixed value accessible from the type itself (e.g., MyEnum.A).
Note The values assigned to an enum value may also be in hexadecimal with an 0x prefix.
Of course, when you wish to define a point of field data within a class or structure, you are not limited to a point of public static literal data. For example, you could update MyBaseClass to support two points of private, instance-level field data, set to default values:
.class public MyBaseClass { .field private string stringField = "hello!" .field private int32 intField = int32(42) }
As in C#, class field data will automatically be initialized to an appropriate default value. If you wish to allow the object user to supply custom values at the time of creation for each of these points of private field data, you (of course) need to create custom constructors.
The CTS supports both instance-level and class-level (static) constructors. In terms of CIL, instance-level constructors are represented using the .ctor token, while a static-level constructor is expressed via .cctor (class constructor). Both of these CIL tokens must be qualified using the rtspecialname (return type special name) and specialname attributes. Simply put, these attributes are used to identify a specific CIL token that can be treated in unique ways by a given .NET language. For example, in C#, constructors do not define a return type; however, in terms of CIL, the return value of a constructor is indeed void:
.class public MyBaseClass { .field private string stringField .field private int32 intField .method public hidebysig specialname rtspecialname instance void .ctor(string s, int32 i) cil managed { // TODO: Add implementation code... } }
Note that the .ctor directive has been qualified with the instance attribute (as it is not a static constructor). The cil managed attributes denote that the scope of this method contains CIL code, rather than unmanaged code, which may be used during platform invocation requests.
Properties and methods also have specific CIL representations. By way of an example, if MyBaseClass were updated to support a public property named TheString, you would author the following CIL (note again the use of the specialname attribute):
.class public MyBaseClass { ... .method public hidebysig specialname instance string get_TheString() cil managed { // TODO: Add implementation code... } .method public hidebysig specialname instance void set_TheString(string 'value') cil managed { // TODO: Add implementation code... } .property instance string TheString() { .get instance string MyNamespace.MyBaseClass::get_TheString() .set instance void MyNamespace.MyBaseClass::set_TheString(string) } }
Recall that in terms of CIL, a property maps to a pair of methods that take get_ and set_ prefixes. The .property directive makes use of the related .get and .set directives to map property syntax to the correct “specially named” methods.
Note Notice that the incoming parameter to the set method of a property is placed in single-tick quotation marks, which represents the name of the token to use on the right-hand side of the assignment operator within the method scope.
In a nutshell, specifying arguments in CIL is (more or less) identical to doing so in C#. For example, each argument is defined by specifying its data type followed by the parameter name. Furthermore, like C#, CIL provides a way to define input, output, and pass-by-reference parameters. As well, CIL allows you to define a parameter array argument (aka the C# params keyword) as well as optional parameters (which are not supported in C# but are used in VB).
To illustrate the process of defining parameters in raw CIL, assume you wish to build a method that takes an int32 (by value), an int32 (by reference), a [mscorlib]System.Collection.ArrayList, and a single output parameter (of type int32). In terms of C#, this method would look something like the following:
public static void MyMethod(int inputInt, ref int refInt, ArrayList ar, out int outputInt) { outputInt = 0; // Just to satisfy the C# compiler... }
If you were to map this method into CIL terms, you would find that C# reference parameters are marked with an ampersand (&) suffixed to the parameter’s underlying data type (int32&).
Output parameters also make use of the & suffix, but they are further qualified using the CIL [out] token. Also notice that if the parameter is a reference type (in this case, the [mscorlib]System.Collections.ArrayList type), the class token is prefixed to the data type (not to be confused with the .class directive!):
.method public hidebysig static void MyMethod(int32 inputInt, int32& refInt, class [mscorlib]System.Collections.ArrayList ar, [out] int32& outputInt) cil managed { ... }